package org.xx.armory.oss.controllers;

import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.ListObjectsRequest;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import org.xx.armory.commons.ForLogging;
import org.xx.armory.oss.FileRef;
import org.xx.armory.oss.OSSResource;
import org.xx.armory.oss.OssAuthorization;
import org.xx.armory.oss.OssDeniedException;

import java.nio.file.Files;
import java.util.concurrent.TimeUnit;

import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.xx.armory.commons.ResponseUtils.encodeRFC2231;
import static org.xx.armory.spring5.mvc.ModelUtils.buildJsonView;
import static org.xx.armory.spring5.mvc.ModelUtils.paramNotBlank;
import static org.xx.armory.spring5.mvc.UploadFileUtils.makeObjectKey;
import static org.xx.armory.spring5.mvc.UploadFileUtils.simpleFileName;

/**
 * hw oss Controller。
 */
@RequestMapping("/aliyun-oss")
public class AliyunOssController {
    public static final String ACTION_DOWNLOAD = "download";

    public static final String ACTION_GET = "get";

    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(AliyunOssController.class);

    @Value("${armory.aliyun-oss.bucket}")
    private String ossBucket;

    @Value("${armory.aliyun-oss.prefix:}")
    private String prefix;

    @Autowired
    private OSSResource ossResource;

    @Autowired(required = false)
    private OssAuthorization ossAuthorization;

    @PostMapping("/{root:[a-z0-9-]+}/{obj-id:\\d+}/{obj-type:\\d+}")
    public ModelAndView upload(
            @PathVariable("root") String root,
            @PathVariable("obj-id") long objId,
            @PathVariable("obj-type") int objType,
            @RequestParam(value = "alt-file-name",
                          required = false) String altFileName,
            @RequestParam("file") MultipartFile file,
            Authentication token
    )
            throws Exception {
        // noinspection ConstantConditions
        final var oFileName = paramNotBlank(file.getOriginalFilename(), "file.name").strip();
        final var fileName = isBlank(altFileName) ? simpleFileName(oFileName) : altFileName.strip();

        if (this.ossAuthorization != null && !this.ossAuthorization.allowUpload(root, objId, objType, fileName, file.getSize(), token)) {
            throw new OssDeniedException();
        }

        try (final var w = this.ossResource.wrap()) {
            final var key = makeObjectKey(this.prefix + root, objId, objType, fileName);
            final var pr = w.getOss().putObject(this.ossBucket, key, file.getInputStream());

            return buildJsonView(new FileRef(fileName, key.substring(this.prefix.length()), file.getSize(), pr.getETag()));
        }
    }

    @GetMapping("/{root:[a-z0-9-]+}/{obj-id:\\d+}/{obj-type:\\d+}")
    public ModelAndView list(
            @PathVariable("root") String root,
            @PathVariable("obj-id") long objId,
            @PathVariable("obj-type") int objType,
            Authentication token
    )
            throws Exception {
        if (this.ossAuthorization != null && !this.ossAuthorization.allowList(root, objId, objType, token)) {
            throw new OssDeniedException();
        }

        final var keyPrefix = makeObjectKey(this.prefix + root, objId, objType, "");
        final var request = new ListObjectsRequest(this.ossBucket).withPrefix(keyPrefix);
        try (final var w = this.ossResource.wrap()) {
            return buildJsonView(
                    w.getOss().listObjects(request)
                     .getObjectSummaries()
                     .stream()
                     .filter(ossObj -> !ossObj.getKey().contentEquals(keyPrefix))
                     .map(ossObj -> new FileRef(
                                  simpleFileName(ossObj.getKey()),
                                  ossObj.getKey().substring(this.prefix.length()),
                                  ossObj.getSize(),
                                  ossObj.getETag()
                          )
                     )
                     .collect(toList())
            );
        }
    }

    @DeleteMapping("/{root:[a-z0-9-]+}/{obj-id:\\d+}/{obj-type:\\d+}/{file-name}")
    public ModelAndView delete(
            @PathVariable("root") String root,
            @PathVariable("obj-id") long objId,
            @PathVariable("obj-type") int objType,
            @PathVariable("file-name") String fileName,
            Authentication token
    )
            throws Exception {
        if (this.ossAuthorization != null && !this.ossAuthorization.allowDelete(root, objId, objType, fileName, token)) {
            throw new OssDeniedException();
        }

        try (final var w = this.ossResource.wrap()) {
            final var key = makeObjectKey(this.prefix + root, objId, objType, fileName);
            w.getOss().deleteObject(this.ossBucket, key);

            return buildJsonView(new FileRef(fileName, key.substring(this.prefix.length())));
        }
    }

    @GetMapping("/{root:[a-z0-9-]+}/{obj-id:\\d+}/{obj-type:\\d+}/{file-name}")
    public ResponseEntity<Resource> download(
            @PathVariable("root") String root,
            @PathVariable("obj-id") long objId,
            @PathVariable("obj-type") int objType,
            @PathVariable("file-name") String fileName,
            @RequestParam(value = "action",
                          defaultValue = ACTION_GET) String action,
            @RequestHeader(value = "If-None-Match",
                           required = false) String ifNoneMatch,
            Authentication token
    )
            throws Exception {
        if (this.ossAuthorization != null && !this.ossAuthorization.allowDownload(root, objId, objType, fileName, token)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }

        try (final var w = this.ossResource.wrap()) {
            final var key = makeObjectKey(this.prefix + root, objId, objType, fileName);
            // 判断oss对象是否存在
            try {
                if (!w.getOss().doesObjectExist(this.ossBucket, key)) {
                    return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
                }
            } catch (OSSException ose1) {
                // OSS的实际行为与文档不一致。如果指定的对象不存在会抛出异常，而不是返回`false`。
                if (ose1.getRawResponseError().contains("NoSuchKey")) {
                    return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
                }
            }

            // 获取oss对象
            final var ossObject = w.getOss().getObject(this.ossBucket, key);
            if (ACTION_GET.equals(action) && ossObject.getObjectMetadata().getETag().equals(ifNoneMatch)) {
                return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
            }

            final var contentType = ossObject.getObjectMetadata().getContentType();

            var builder = ResponseEntity.ok()
                                        .contentLength(ossObject.getObjectMetadata().getContentLength());

            if (contentType.startsWith("text/") && !contentType.contains(";")) {
                builder = builder.contentType(MediaType.valueOf(contentType + ";charset=UTF8"));
            } else {
                builder = builder.contentType(MediaType.valueOf(contentType));
            }
            if (ACTION_GET.equals(action)) {
                builder = builder
                        .cacheControl(CacheControl.maxAge(60 * 24, TimeUnit.MINUTES))
                        .eTag(ossObject.getObjectMetadata().getETag())
                        .varyBy("If-None-Match");
            } else if (ACTION_DOWNLOAD.equals(action)) {
                builder = builder.header("Content-Disposition", "attachment;filename*=" + encodeRFC2231(simpleFileName(ossObject.getKey())));
            }

            if (ossObject.getObjectMetadata().getContentLength() > 2 * 1024 * 1024) {
                // 如果文件超过2M bytes，那么保存到临时文件中。
                final var tempFile = Files.createTempFile("oss-", "." + FilenameUtils.getExtension(key));
                return builder.body(new FileSystemResource(tempFile));
            } else {
                // 否则保存在字节数组中。
                final var bytes = FileCopyUtils.copyToByteArray(ossObject.getObjectContent());
                return builder.body(new ByteArrayResource(bytes));
            }
        }
    }
}
